﻿using System;
using System.Threading;

namespace CircuitBreaker
{
    public class CircuitBreaker
    {
        private readonly object monitor = new object();
        private CircuitBreakerState state;
        private readonly object openedStateSyncRoot = new object();
        private readonly object closedStateSyncRoot = new object();
        private readonly object halfOpenedStateSyncRoot = new object();
        private Timer _failingCountResetTimer;
        private long _failuresResetPeriod;
        private string _name;
        private readonly object _initSyncRoot = new object();
        private bool _initialized = false;

        public string Name
        {
            get { return _name; }
        }

        internal CircuitBreaker(CircuitBreakerSettings settings)
            : this(settings.Name, settings.Threshold, settings.PeriodToHalfOpen, settings.FailuresResetPeriod)
        {
        }
        private CircuitBreaker(string name, int threshold, long periodToHalfOpen, long failuresResetPeriod)
        {
            _name = name + "_" + this.GetHashCode();
            if (threshold < 1)
            {
                throw new ArgumentOutOfRangeException("threshold", "Threshold should be greater than 0");
            }

            if (periodToHalfOpen < 1)
            {
                throw new ArgumentOutOfRangeException("periodToHalfOpen", "PeriodToHalfOpen should be greater than 0");
            }

            Threshold = threshold;
            PeriodToHalfOpen = periodToHalfOpen;
            _failuresResetPeriod = failuresResetPeriod;
#if DEBUG
            Console.WriteLine(_name + ": created");
#endif

            _failingCountResetTimer = new Timer(new TimerCallback(t =>
            {
                //reset failure count on schedule
                ResetFailureCount();

            }), null, Timeout.Infinite, Timeout.Infinite);
            Close();
        }



        private int failures;
        public int Failures
        {
            get { return failures; }
        }
        public int Threshold { get; private set; }
        public long PeriodToHalfOpen { get; private set; }

        public bool IsClosed
        {
            get { return state is ClosedState; }
        }

        public bool IsOpened
        {
            get { return state is OpenState; }
        }

        public bool IsHalfOpened
        {
            get { return state is HalfOpenState; }
        }


        internal int IncreaseFailureCount()
        {
            int failureCount = Interlocked.Increment(ref failures);
            return failureCount;
        }

        private void RescheduleFailingCountTimer()
        {
            _failingCountResetTimer.Change(_failuresResetPeriod, Timeout.Infinite);
        }

        private void ResetFailureCount()
        {

            Interlocked.Exchange(ref failures, 0);
            RescheduleFailingCountTimer();
            //#if DEBUG
            //            Console.WriteLine("fail count reseted");
            //#endif
        }


        public bool ThresholdReached(int failures)
        {
            return failures >= Threshold;
        }
        public bool ThresholdReached()
        {
            return Failures >= Threshold;
        }

        public void AttemptCall(Action protectedCode)
        {
            state.ProtectedCodeIsAboutToBeCalled();

            try
            {
                protectedCode();
            }
            catch (Exception e)
            {
                state.ActUponException(e);
                throw;
            }

            state.ProtectedCodeHasBeenCalled();
        }
        public TResult AttemptCall<T, TResult>(Func<T, TResult> protectedCode, T arg)
        {
            TResult result = default(TResult);
            state.ProtectedCodeIsAboutToBeCalled();

            try
            {
                result = protectedCode(arg);
            }
            catch (Exception e)
            {
                state.ActUponException(e);
                throw;
            }

            state.ProtectedCodeHasBeenCalled();
            return result;
        }

        public TResult AttemptCall<T1, T2, TResult>(Func<T1, T2, TResult> protectedCode, T1 arg1, T2 arg2)
        {
            TResult result = default(TResult);
            state.ProtectedCodeIsAboutToBeCalled();

            try
            {
                result = protectedCode(arg1, arg2);
            }
            catch (Exception e)
            {
                state.ActUponException(e);
                throw;
            }

            state.ProtectedCodeHasBeenCalled();
            return result;
        }


        public TResult AttemptCall<T1, T2, T3, TResult>(Func<T1, T2, T3, TResult> protectedCode, T1 arg1, T2 arg2, T3 arg3)
        {
            TResult result = default(TResult);
            state.ProtectedCodeIsAboutToBeCalled();

            try
            {
                result = protectedCode(arg1, arg2, arg3);
            }
            catch (Exception e)
            {
                state.ActUponException(e);
                throw;
            }

            state.ProtectedCodeHasBeenCalled();
            return result;
        }

        internal void Close()
        {
            if (!IsClosed && Monitor.TryEnter(closedStateSyncRoot))
            {
                try
                {
                    if (!IsClosed)
                    {
                        ResetFailureCount();
                        state = new ClosedState(this);
#if DEBUG
                        Console.ForegroundColor = ConsoleColor.Green;
                        Console.WriteLine(string.Format("{0}: circuit breaker closed", _name));
                        Console.ForegroundColor = ConsoleColor.Gray;
#endif
                    }
                }
                finally
                {

                    Monitor.Exit(closedStateSyncRoot);
                }

            }
            //using (TimedLock.Lock(closedStateSyncRoot))
            //{
            //    MoveToClosedState();
            //}
        }

        internal void Open()
        {

            if (!IsOpened && Monitor.TryEnter(openedStateSyncRoot))
            {
                try
                {
                    if (!IsOpened)
                    {
                        state = new OpenState(this);
#if DEBUG
                        Console.ForegroundColor = ConsoleColor.Red;
                        Console.WriteLine(string.Format("{0}: circuit breaker  opened", _name));
                        Console.ForegroundColor = ConsoleColor.Gray;
#endif
                    }
                }
                finally
                {

                    Monitor.Exit(openedStateSyncRoot);
                }

            }
        }

        internal void HalfOpen()
        {
            if (!IsHalfOpened && Monitor.TryEnter(halfOpenedStateSyncRoot))
            {
                try
                {
                    if (!IsHalfOpened)
                    {
                        state = new HalfOpenState(this);
#if DEBUG
                        Console.ForegroundColor = ConsoleColor.Yellow;
                        Console.WriteLine(string.Format("{0}: circuit breaker half-opened", _name));
                        Console.ForegroundColor = ConsoleColor.White;
#endif
                    }
                }
                finally
                {

                    Monitor.Exit(halfOpenedStateSyncRoot);
                }

            }
        }
    }
}